package com.zusmart.base.buffer.support;

import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.InvalidMarkException;
import java.nio.ReadOnlyBufferException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;

import com.zusmart.base.buffer.Buffer;
import com.zusmart.base.util.Assert;

public abstract class AbstractBuffer implements Buffer {

	private static final ReleasedBufferException RELEASED_BUFFER_EXCEPTION = new ReleasedBufferException();

	private boolean released;
	private boolean readonly;
	private boolean permanent;
	private boolean bigEndian = false;

	private int mark = -1;
	private int limit = 0;
	private int position = 0;
	private int capacity = 0;

	private final int offset;

	protected AbstractBuffer(int offset, int capacity) {
		this.offset = offset;
		this.capacity = capacity;
		this.limit(capacity);
	}

	@Override
	public void release() {
		if (!(this.permanent || this.released)) {
			try {
				this.doRelease();
			} finally {
				this.released = true;
			}
		}
	}

	@Override
	public boolean isReleased() {
		return this.released;
	}

	@Override
	public boolean isPermanent() {
		return this.permanent;
	}

	@Override
	public boolean isReadOnly() {
		return this.readonly;
	}

	@Override
	public boolean isBigEndian() {
		return this.bigEndian;
	}

	@Override
	public boolean hasRemaining() {
		return this.position < this.limit;
	}

	@Override
	public int indexOf(byte b) {
		int startIndex = this.getIndex(0);
		int endIndex = startIndex + this.remaining() - 1;
		for (int i = startIndex; i <= endIndex; i++) {
			if (this.doGet(i) == b) {
				return i - startIndex + this.position;
			}
		}
		return -1;
	}

	@Override
	public int indexOf(byte[] b) {
		if (b.length == 0) {
			return this.position;
		}
		int startIndex = this.getIndex(0);
		int endIndex = startIndex + this.remaining() - b.length;
		byte first = b[0];
		Label: for (int i = startIndex; i <= endIndex; i++) {
			if (this.doGet(i) == first) {
				for (int j = 1; j < b.length; j++) {
					if (this.doGet(i + j) != b[j]) {
						continue Label;
					}
				}
				return i - startIndex + this.position;
			}
		}
		return -1;
	}

	@Override
	public int read(ReadableByteChannel channel) throws IOException {
		int count = 0;
		try {
			count = channel.read(this.asByteBuffer());
		} finally {
			if (count > 0) {
				this.skip(count);
			}
		}
		return count;
	}

	@Override
	public int write(WritableByteChannel channel) throws IOException {
		int count = 0;
		try {
			count = channel.write(this.asByteBuffer());
		} finally {
			if (count > 0) {
				this.skip(count);
			}
		}
		return count;
	}

	@Override
	public int capacity() {
		return this.capacity;
	}

	@Override
	public int limit() {
		return this.limit;
	}

	@Override
	public int position() {
		return this.position;
	}

	@Override
	public int remaining() {
		return this.limit - this.position;
	}

	@Override
	public Buffer reset() {
		if (this.mark < 0) {
			throw new InvalidMarkException();
		}
		this.position = this.mark;
		return this;
	}

	@Override
	public Buffer clear() {
		this.mark = -1;
		this.position = 0;
		this.limit = this.capacity;
		return this;
	}

	@Override
	public Buffer flip() {
		this.limit = this.position;
		this.position = 0;
		this.mark = -1;
		return this;
	}

	@Override
	public Buffer mark() {
		return this.mark(this.position);
	}

	@Override
	public Buffer limit(int limit) {
		Assert.isTrue(limit > this.capacity || limit < 0, String.format("limit must >= 0 and <= %d", this.capacity));
		this.limit = limit;
		return this.position(Math.min(this.position, limit));
	}

	@Override
	public Buffer position(int position) {
		Assert.isNull(position > this.limit || position < 0, String.format("position must <= %d and >= 0", this.limit));
		this.position = position;
		if (this.mark > this.position) {
			this.mark = -1;
		}
		return this;
	}

	@Override
	public Buffer capacity(int capacity) {
		Assert.isTrue(capacity < 0, "capacity must >= 0");
		this.capacity = capacity;
		return this.limit(Math.min(this.limit, this.capacity));
	}

	@Override
	public Buffer skip(int size) {
		if (size != 0) {
			this.position(this.position + size);
		}
		return this;
	}

	@Override
	public Buffer rewind() {
		this.position = 0;
		this.mark = -1;
		return this;
	}

	@Override
	public Buffer setPermanent(boolean permanent) {
		this.checkReleased();
		this.permanent = permanent;
		return this;
	}

	@Override
	public Buffer setBigEndian(boolean bigEndian) {
		this.bigEndian = bigEndian;
		return this;
	}

	@Override
	public Buffer asReadOnlyBuffer() {
		return ((AbstractBuffer) this.duplicate()).setReadOnly(true);
	}

	///////////////////////////////////////////////////////////////////

	@Override
	public byte get() {
		return this.doGet(this.getIndex(1));
	}

	@Override
	public byte get(int index) {
		return this.doGet(this.getIndex(index, 1));
	}

	@Override
	public Buffer get(byte[] dst) {
		return this.get(dst, 0, dst.length);
	}

	@Override
	public Buffer get(int index, byte[] dst) {
		return this.get(index, dst, 0, dst.length);
	}

	@Override
	public Buffer get(byte[] dst, int offset, int length) {
		return this.get(ByteBuffer.wrap(dst, offset, length));
	}

	@Override
	public Buffer get(int index, byte[] dst, int offset, int length) {
		return this.get(index, ByteBuffer.wrap(dst, offset, length));
	}

	@Override
	public Buffer get(ByteBuffer dst) {
		return this.get(dst, dst.remaining());
	}

	@Override
	public Buffer get(int index, ByteBuffer dst) {
		return this.get(index, dst, dst.remaining());
	}

	@Override
	public Buffer get(Buffer dst, int length) {
		checkBounds(0, length, dst.remaining());
		this.getIndex(length);
		ByteBuffer buffer = this.asByteBuffer();
		buffer.limit(buffer.position());
		buffer.position(buffer.position() - length);
		dst.put(buffer);
		return this;
	}

	@Override
	public Buffer get(int index, Buffer dst, int length) {
		checkBounds(0, length, dst.remaining());
		this.getIndex(index, length);
		ByteBuffer buffer = this.asByteBuffer();
		buffer.position(index);
		buffer.limit(index + length);
		dst.put(buffer);
		return this;
	}

	@Override
	public Buffer get(Buffer dst) {
		return this.get(dst, dst.remaining());
	}

	@Override
	public Buffer get(int index, Buffer dst) {
		return this.get(index, dst, dst.remaining());
	}

	@Override
	public Buffer get(ByteBuffer dst, int length) {
		checkBounds(0, length, dst.remaining());
		this.getIndex(length);
		ByteBuffer buffer = this.asByteBuffer();
		buffer.limit(buffer.position());
		buffer.position(buffer.position() - length);
		dst.put(buffer);
		return this;
	}

	@Override
	public Buffer get(int index, ByteBuffer dst, int length) {
		checkBounds(0, length, dst.remaining());
		this.getIndex(index, length);
		ByteBuffer buffer = this.asByteBuffer();
		buffer.position(index);
		buffer.limit(index + length);
		dst.put(buffer);
		return this;
	}

	@Override
	public char getChar() {
		return (char) this.getShort();
	}

	@Override
	public char getChar(int index) {
		return (char) this.getShort(index);
	}

	@Override
	public short getShort() {
		return decodeShort(this, this.getIndex(2));
	}

	@Override
	public short getShort(int index) {
		return decodeShort(this, this.getIndex(index, 2));
	}

	@Override
	public int getInt() {
		return decodeInt(this, this.getIndex(4));
	}

	@Override
	public int getInt(int index) {
		return decodeInt(this, this.getIndex(index, 4));
	}

	@Override
	public long getLong() {
		return decodeLong(this, this.getIndex(8));
	}

	@Override
	public long getLong(int index) {
		return decodeLong(this, this.getIndex(index, 8));
	}

	@Override
	public float getFloat() {
		return Float.intBitsToFloat(this.getInt());
	}

	@Override
	public float getFloat(int index) {
		return Float.intBitsToFloat(this.getInt(index));
	}

	@Override
	public double getDouble() {
		return Double.longBitsToDouble(this.getLong());
	}

	@Override
	public double getDouble(int index) {
		return Double.longBitsToDouble(this.getLong(index));
	}

	@Override
	public short getUnsignedByte() {
		return (short) (this.get() & 0xff);
	}

	@Override
	public short getUnsignedByte(int index) {
		return (short) (this.get(index) & 0xff);
	}

	@Override
	public int getUnsignedShort() {
		return this.getShort() & 0xffff;
	}

	@Override
	public int getUnsignedShort(int index) {
		return this.getShort(index) & 0xffff;
	}

	@Override
	public long getUnsignedInt() {
		return this.getInt() & 0xffffffffL;
	}

	@Override
	public long getUnsignedInt(int index) {
		return this.getInt(index) & 0xffffffffL;
	}

	@Override
	public Buffer put(byte b) {
		this.doPut(this.putIndex(1), b);
		return this;
	}

	@Override
	public Buffer put(int index, byte b) {
		this.doPut(this.putIndex(index, 1), b);
		return this;
	}

	@Override
	public Buffer put(byte[] src) {
		return this.put(src, 0, src.length);
	}

	@Override
	public Buffer put(int index, byte[] src) {
		return this.put(index, src, 0, src.length);
	}

	@Override
	public Buffer put(byte[] src, int offset, int length) {
		return this.put(ByteBuffer.wrap(src, offset, length));
	}

	@Override
	public Buffer put(int index, byte[] src, int offset, int length) {
		return this.put(index, ByteBuffer.wrap(src, offset, length));
	}

	@Override
	public Buffer put(ByteBuffer src) {
		return this.put(src, src.remaining());
	}

	@Override
	public Buffer put(int index, ByteBuffer src) {
		return this.put(index, src, src.remaining());
	}

	@Override
	public Buffer put(ByteBuffer src, int length) {
		checkBounds(0, length, src.remaining());
		int pos = this.putIndex(length);
		for (int i = 0; i < length; i++) {
			this.doPut(pos + i, src.get());
		}
		return this;
	}

	@Override
	public Buffer put(int index, ByteBuffer src, int length) {
		checkBounds(0, length, src.remaining());
		int pos = this.putIndex(index, length);
		for (int i = 0; i < length; i++) {
			this.doPut(pos + i, src.get());
		}
		return this;
	}

	@Override
	public Buffer put(Buffer src) {
		return this.put(src, src.remaining());
	}

	@Override
	public Buffer put(int index, Buffer src) {
		return this.put(index, src, src.remaining());
	}

	@Override
	public Buffer put(Buffer src, int length) {
		checkBounds(0, length, src.remaining());
		int pos = this.putIndex(length);
		for (int i = 0; i < length; i++) {
			this.doPut(pos + i, src.get());
		}
		return this;
	}

	@Override
	public Buffer put(int index, Buffer src, int length) {
		checkBounds(0, length, src.remaining());
		int pos = this.putIndex(index, length);
		for (int i = 0; i < length; i++) {
			this.doPut(pos + i, src.get());
		}
		return this;
	}

	@Override
	public Buffer putChar(char c) {
		return this.putShort((short) c);
	}

	@Override
	public Buffer putChar(int index, char c) {
		return this.putShort(index, (short) c);
	}

	@Override
	public Buffer putShort(short s) {
		return encodeShort(this, this.putIndex(2), s);
	}

	@Override
	public Buffer putShort(int index, short s) {
		return encodeShort(this, this.putIndex(index, 2), s);
	}

	@Override
	public Buffer putInt(int i) {
		return encodeInt(this, this.putIndex(4), i);
	}

	@Override
	public Buffer putInt(int index, int i) {
		return encodeInt(this, this.putIndex(index, 4), i);
	}

	@Override
	public Buffer putLong(long l) {
		return encodeLong(this, this.putIndex(8), l);
	}

	@Override
	public Buffer putLong(int index, long l) {
		return encodeLong(this, this.putIndex(index, 8), l);
	}

	@Override
	public Buffer putFloat(float f) {
		return this.putInt(Float.floatToRawIntBits(f));
	}

	@Override
	public Buffer putFloat(int index, float f) {
		return this.putInt(index, Float.floatToRawIntBits(f));
	}

	@Override
	public Buffer putDouble(double d) {
		return this.putLong(Double.doubleToRawLongBits(d));
	}

	@Override
	public Buffer putDouble(int index, double d) {
		return this.putLong(index, Double.doubleToRawLongBits(d));
	}

	@Override
	public Buffer putUnsignedByte(short s) {
		if (s < 0 || s > 0xff) {
			throw new IllegalArgumentException();
		}
		return this.put((byte) s);
	}

	@Override
	public Buffer putUnsignedByte(int index, short s) {
		if (s < 0 || s > 0xff) {
			throw new IllegalArgumentException();
		}
		return this.put(index, (byte) s);
	}

	@Override
	public Buffer putUnsignedShort(int i) {
		if (i < 0 || i > 0xffff) {
			throw new IllegalArgumentException();
		}
		return this.putShort((short) i);
	}

	@Override
	public Buffer putUnsignedShort(int index, int i) {
		if (i < 0 || i > 0xffff) {
			throw new IllegalArgumentException();
		}
		return this.putShort(index, (short) i);
	}

	@Override
	public Buffer putUnsignedInt(long l) {
		if (l < 0 || l > 0xffffffff) {
			throw new IllegalArgumentException();
		}
		return this.putInt((int) l);
	}

	@Override
	public Buffer putUnsignedInt(int index, long l) {
		if (l < 0 || l > 0xffffffff) {
			throw new IllegalArgumentException();
		}
		return this.putInt(index, (int) l);
	}

	///////////////////////////////////////////////////////////////////

	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof Buffer)) {
			return false;
		}
		Buffer buffer = (Buffer) obj;
		if (this.remaining() != buffer.remaining()) {
			return false;
		}
		int start = this.getIndex(0);
		int end = start + this.remaining();
		for (int i = start, j = buffer.position(); i < end; i++, j++) {
			if (this.doGet(i) != buffer.get(j)) {
				return false;
			}
		}
		return true;
	}

	@Override
	public String toString() {
		StringBuffer sb = new StringBuffer();
		sb.append(this.getClass().getName());
		sb.append("[position=").append(this.position).append(" ");
		sb.append("limit=").append(this.limit).append(" ");
		sb.append("capacity=").append(this.capacity).append("]");
		return sb.toString();
	}

	protected abstract byte doGet(int index);

	protected abstract void doPut(int index, byte b);

	protected abstract void doRelease();

	protected int getMark() {
		return this.mark;
	}

	protected Buffer setReadOnly(boolean readonly) {
		this.readonly = readonly;
		return this;
	}

	protected Buffer mark(int mark) {
		if (mark < 0) {
			this.mark = -1;
		} else {
			if (mark > this.position) {
				throw new IndexOutOfBoundsException();
			}
			this.mark = mark;
		}
		return this;
	}

	protected final int putIndex(int i, int len) {
		this.checkReleased();
		this.checkReadOnly();
		if (this.limit - this.position < len) {
			throw new BufferOverflowException();
		}
		int pos = this.position + this.offset;
		this.position += len;
		return pos;
	}

	protected final int putIndex(int len) {
		this.checkReleased();
		this.checkReadOnly();
		if (this.limit - this.position < len) {
			throw new BufferOverflowException();
		}
		int pos = this.position + this.offset;
		this.position += len;
		return pos;
	}

	protected final int getIndex(int i, int len) {
		this.checkReleased();
		if ((len < 0) || (i < 0) || (len > this.limit - i)) {
			throw new IndexOutOfBoundsException();
		}
		return i + this.offset;
	}

	protected final int getIndex(int len) {
		this.checkReleased();
		if (this.limit - this.position < len) {
			throw new BufferUnderflowException();
		}
		int pos = this.position + this.offset;
		this.position += len;
		return pos;
	}

	protected final void checkReadOnly() {
		if (this.isReadOnly()) {
			throw new ReadOnlyBufferException();
		}
	}

	protected final void checkReleased() {
		if (this.isReleased()) {
			throw RELEASED_BUFFER_EXCEPTION;
		}
	}

	protected final static void checkBounds(int off, int len, int size) {
		if ((off | len | (off + len) | (size - (off + len))) < 0) {
			throw new IndexOutOfBoundsException();
		}
	}

	protected final static Buffer encodeShort(AbstractBuffer buffer, int index, short s) {
		byte b1, b2;
		if (buffer.isBigEndian()) {
			b1 = (byte) (s >> 8);
			b2 = (byte) (s >> 0);
		} else {
			b1 = (byte) (s >> 0);
			b2 = (byte) (s >> 8);
		}
		buffer.doPut(index + 0, b1);
		buffer.doPut(index + 1, b2);
		return buffer;
	}

	protected final static short decodeShort(AbstractBuffer buffer, int index) {
		byte b1, b2;
		if (buffer.isBigEndian()) {
			b1 = buffer.doGet(index + 0);
			b2 = buffer.doGet(index + 1);
		} else {
			b1 = buffer.doGet(index + 1);
			b2 = buffer.doGet(index + 0);
		}
		return (short) ((b1 << 8) | (b2 & 0xff));
	}

	protected final static Buffer encodeInt(AbstractBuffer buffer, int index, int i) {
		byte b1, b2, b3, b4;
		if (buffer.isBigEndian()) {
			b1 = (byte) (i >> 24);
			b2 = (byte) (i >> 16);
			b3 = (byte) (i >> 8);
			b4 = (byte) (i >> 0);
		} else {
			b1 = (byte) (i >> 0);
			b2 = (byte) (i >> 8);
			b3 = (byte) (i >> 16);
			b4 = (byte) (i >> 24);
		}
		buffer.doPut(index + 0, b1);
		buffer.doPut(index + 1, b2);
		buffer.doPut(index + 2, b3);
		buffer.doPut(index + 3, b4);
		return buffer;
	}

	protected final static int decodeInt(AbstractBuffer buffer, int index) {
		byte b1, b2, b3, b4;
		if (buffer.isBigEndian()) {
			b1 = buffer.doGet(index + 0);
			b2 = buffer.doGet(index + 1);
			b3 = buffer.doGet(index + 2);
			b4 = buffer.doGet(index + 3);
		} else {
			b1 = buffer.doGet(index + 3);
			b2 = buffer.doGet(index + 2);
			b3 = buffer.doGet(index + 1);
			b4 = buffer.doGet(index + 0);
		}
		return (int) ((((b1 & 0xff) << 24) | ((b2 & 0xff) << 16) | ((b3 & 0xff) << 8) | ((b4 & 0xff) << 0)));
	}

	protected final static Buffer encodeLong(AbstractBuffer buffer, int index, long l) {
		byte b1, b2, b3, b4, b5, b6, b7, b8;
		if (buffer.isBigEndian()) {
			b1 = (byte) (l >> 56);
			b2 = (byte) (l >> 48);
			b3 = (byte) (l >> 40);
			b4 = (byte) (l >> 32);
			b5 = (byte) (l >> 24);
			b6 = (byte) (l >> 16);
			b7 = (byte) (l >> 8);
			b8 = (byte) (l >> 0);
		} else {
			b1 = (byte) (l >> 0);
			b2 = (byte) (l >> 8);
			b3 = (byte) (l >> 16);
			b4 = (byte) (l >> 24);
			b5 = (byte) (l >> 32);
			b6 = (byte) (l >> 40);
			b7 = (byte) (l >> 48);
			b8 = (byte) (l >> 56);
		}
		buffer.doPut(index + 0, b1);
		buffer.doPut(index + 1, b2);
		buffer.doPut(index + 2, b3);
		buffer.doPut(index + 3, b4);
		buffer.doPut(index + 4, b5);
		buffer.doPut(index + 5, b6);
		buffer.doPut(index + 6, b7);
		buffer.doPut(index + 7, b8);
		return buffer;
	}

	protected final static long decodeLong(AbstractBuffer buffer, int index) {
		byte b1, b2, b3, b4, b5, b6, b7, b8;
		if (buffer.isBigEndian()) {
			b1 = buffer.doGet(index + 0);
			b2 = buffer.doGet(index + 1);
			b3 = buffer.doGet(index + 2);
			b4 = buffer.doGet(index + 3);
			b5 = buffer.doGet(index + 4);
			b6 = buffer.doGet(index + 5);
			b7 = buffer.doGet(index + 6);
			b8 = buffer.doGet(index + 7);
		} else {
			b1 = buffer.doGet(index + 7);
			b2 = buffer.doGet(index + 6);
			b3 = buffer.doGet(index + 5);
			b4 = buffer.doGet(index + 4);
			b5 = buffer.doGet(index + 3);
			b6 = buffer.doGet(index + 2);
			b7 = buffer.doGet(index + 1);
			b8 = buffer.doGet(index + 0);
		}
		return (((long) b1 & 0xff) << 56) | (((long) b2 & 0xff) << 48) | (((long) b3 & 0xff) << 40) | (((long) b4 & 0xff) << 32) | (((long) b5 & 0xff) << 24) | (((long) b6 & 0xff) << 16) | (((long) b7 & 0xff) << 8) | (((long) b8 & 0xff) << 0);
	}

	public static class ReleasedBufferException extends RuntimeException {

		private static final long serialVersionUID = -8495651006197186862L;

		public ReleasedBufferException() {
			super();
		}

		public ReleasedBufferException(String message, Throwable cause) {
			super(message, cause);
		}

		public ReleasedBufferException(String message) {
			super(message);
		}

		public ReleasedBufferException(Throwable cause) {
			super(cause);
		}

	}

}